/*
* Copyright 2011 Pavel Kozhin 
*
* Licensed under the Apache License, Version 2.0 (the "License"); 
* you may not use this file except in compliance with the License. 
* You may obtain a copy of the License at 
* 
* http://www.apache.org/licenses/LICENSE-2.0
* 
* Unless required by applicable law or agreed to in writing, software 
* distributed  under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
* See the License for the specific language governing permissions and 
* limitations under the License.
*
* Author: Pavel Kozhin.
*/
package org.etherframework.mediation.manager
{
	import flash.errors.IllegalOperationError;
	import flash.events.Event;
	import flash.events.IEventDispatcher;
	import flash.sampler.getSavedThis;
	import flash.system.ApplicationDomain;
	import flash.utils.Dictionary;
	
	import org.etherframework.IEther;
	import org.etherframework.core.common.IDestroyableNotifier;
	import org.etherframework.core.common.IInstanceFactory;
	import org.etherframework.core.utils.ClassType;
	import org.etherframework.core.utils.ClassUtils;
	import org.etherframework.mediation.IRUID;
	import org.etherframework.mediation.core.BunchHelper;
	import org.etherframework.mediation.core.MediationData;
	import org.etherframework.mediation.core.MediationEvent;
	import org.etherframework.mediation.core.MediationFactory;
	import org.etherframework.mediation.core.MediationHandlers;
	import org.etherframework.mediation.core.Provider;
	import org.etherframework.mediation.core.ProviderData;
	import org.floxy.IProxyRepository;

	// TODO: Refactor.
	[ExcludeClass]
	public class MediationManager implements IMediationManager, IInstanceFactory
	{
		private var _repository:IProxyRepository;
		private var _initialized:Boolean = false;
		private var _classes:Array = [];
		private var _factory:MediationFactory;
		private var _applicationDomain:ApplicationDomain;
		private var _mediationData:MediationData;
		private var _providerData:ProviderData;
		private var _map:Dictionary = new Dictionary();
		
		public function MediationManager()
		{
			_factory = new MediationFactory(this, null);
			_applicationDomain = ApplicationDomain.currentDomain;
		}
		
		public function initialize(proxyRepository:Object, mediationData:MediationData, 
								   providerData:ProviderData):void
		{
			if (_initialized)
				throw new IllegalOperationError("MediationManager is already initialized.");
			if (!proxyRepository)
				throw new ArgumentError("Argument proxyRepository:Object can not be null.");
			if (!mediationData)
				throw new ArgumentError("Argument mediationData:MediationData can not be null.");
			if (!providerData)
				throw new ArgumentError("Argument providerData:ProviderData can not be null.");
			if (!(proxyRepository is IProxyRepository))
				throw new ArgumentError("Argument proxyRepository:Object should " +
					"implement IProxyRepository interface.");
			
			_repository = IProxyRepository(proxyRepository);
			_factory = new MediationFactory(this, _repository);
			_mediationData = mediationData;
			_providerData = providerData;
			_initialized = true;
		}
		
		public function prepare(classes:Array, bunch:int = -1):IEventDispatcher
		{
			ensureInitialized();
			
			if (!(classes && classes.length))
				throw new ArgumentError("Argument classes:Array can not be null or empty.");
			
			bunch = (bunch > 0 ? bunch : classes.length);
			
			return getCompleteDispatcher(classes, 
				new BunchHelper(this, bunch).prepare(classes));
		}
		
		public function prepareClasses(classes:Array):IEventDispatcher
		{
			ensureInitialized();
			
			if (!(classes && classes.length))
				throw new ArgumentError("Argument classes:Array can not be null or empty.");
			
			return getCompleteDispatcher(classes, _repository.prepare(classes, 
				_applicationDomain));
		}
		
		public function createInstance(generator:Class, args:Array, ether:IEther):Object
		{
			return _factory.createInstance(generator, args, ether);
		}
		
		public function hasMediation(type:Class):Boolean
		{
			return (_classes.indexOf(type) != -1);
		}
		
		public function createProvider(ether:IEther):Provider
		{
			return new Provider(ether, this);
		}
		
		public function bindMethodAsTerm(ether:IEther, handler:Function):void
		{
			if (!_providerData.uid)
				throw new Error("Please, make sure that you use prepared " +
					"IMediationProvider object for binding.");
			if ((_providerData.isGetter || _providerData.isSetter))
				throw new Error("Can't bind to a method. Member \"" + 
					_providerData.name + "\" is not a method.");
			if (!(handler is Function))
				throw new ArgumentError("Argument handler:Function can not be null.");
			
			addHandler(_providerData.uid, 
				getMediationId(_providerData.uid, _mediationData.context, _providerData.name), 
				ether, handler);
		}
		
		public function bindMethod(ether:IEther, method:Function, handler:Function):void
		{
			// Would be better to have ether.mediations.bind.method(model.updateUser, handler)
			// instead of ether.mediations.bind.method(model.updateUser(user, foo), handler).
			// But currently there is no way to enumerate proxies w/o interception.
		}
		
		public function unbindMethod(ether:IEther, method:Function, handler:Function):void
		{
			
		}
		
		public function bindProperty(ether:IEther, setter:Function, getter:Function):void
		{
			if (!_providerData.uid)
				throw new Error("Please, make sure that you use prepared " +
					"IMediationProvider object for binding.");
			if (!(_providerData.isGetter || _providerData.isSetter))
				throw new Error("Can't bind to a property. Member \"" + 
					_providerData.name + "\" is not a property.");
			
			if (setter is Function)
			{
				addHandler(_providerData.uid, 
					getMediationId(_providerData.uid, _mediationData.context, _providerData.name, false, true), 
					ether, setter);
			}
			if (getter is Function)
			{
				addHandler(_providerData.uid, 
					getMediationId(_providerData.uid, _mediationData.context, _providerData.name, true), 
					ether, getter);
			}
		}
		
		public function unbindMethodAsTerm(ether:IEther, handler:Function):void
		{
			if (!_providerData.uid)
				throw new Error("Please, make sure that you use prepared " +
					"IMediationProvider object for binding.");
			
			if (!(handler is Function))
				throw new ArgumentError("Argument handler:Function can not be null.");
			
			removeHandler(_providerData.uid, 
				getMediationId(_providerData.uid, _mediationData.context, _providerData.name), 
				ether, handler);
		}
		
		public function unbindProperty(ether:IEther, setter:Function, getter:Function):void
		{
			if (!_providerData.uid)
				throw new Error("Please, make sure that you use prepared " +
					"IMediationProvider object for binding.");
			
			if (setter is Function)
			{
				removeHandler(_providerData.uid, 
					getMediationId(_providerData.uid, _mediationData.context, _providerData.name, false, true), 
					ether, setter);
			}
			if (getter is Function)
			{
				removeHandler(_providerData.uid, 
					getMediationId(_providerData.uid, _mediationData.context, _providerData.name, true), 
					ether, getter);
			}
		}
		
		public function getMediationId(targetId:String, context:String, name:String, 
									   isGetter:Boolean = false, isSetter:Boolean = false):String
		{
			return targetId + context + name + 
				((isGetter) ? "__getter" : ((isSetter ? "__setter" : "__method")));
		}
		
		public function hasHandlers(uid:String, id:String):Boolean
		{
			return _map[uid] ? MediationHandlers(_map[uid]).hasHandlers(id) : false;
		}
		
		public function clearMediations(ether:IEther):void
		{
			const uid:String = IRUID(ether).uid;
			const handlers:MediationHandlers = _map[uid];
			
			if (handlers)
			{
				handlers.walk(function(id:String, items:Vector.<Function>):void
				{
					for (var i:int = 0; i < items.length; i++) 
					{
						ether.unlisten(id, items[i]);
					}
				});
				
				delete _map[uid];
			}
		}
		
		private function addHandler(uid:String, id:String, ether:IEther, handler:Function):void
		{
			const handlers:MediationHandlers = _map[uid] || 
				(_map[uid] = new MediationHandlers(uid)); 
			
			if (!handlers.hasHandlers(id))
			{
				ether.listen(id, allMediationsHandler);
			}
			
			handlers.addHandler(id, handler);
			
			IDestroyableNotifier(ether).addHandler(destryEtherHandler);
		}
		
		private function removeHandler(uid:String, id:String, ether:IEther, handler:Function):void
		{
			const handlers:MediationHandlers = _map[uid];
			
			if (handlers)
			{
				handlers.removeHandler(id, handler);
			}
			ether.unlisten(id, allMediationsHandler);
		}
		
		private function allMediationsHandler(event:MediationEvent):void
		{
			const handlers:MediationHandlers = _map[_providerData.uid];
			
			if (handlers)
			{
				const h:Vector.<Function> = handlers.getHandlers(event.type);
				const data:* = _providerData.getValue(event.type) || null;
					
				for (var i:int = 0; i < h.length; i++) 
				{
					if (_providerData.isSetter)
					{
						h[i].apply(null, data);
					}
					else
					{
						_mediationData.returnResult.push(h[i].apply(null, data));
					}
				}
			}
		}
		
		private function destryEtherHandler(ether:IDestroyableNotifier):void
		{
			delete _map[ether.uid];
		}
		
		private function getCompleteDispatcher(classes:Array, dispatcher:IEventDispatcher):IEventDispatcher
		{
			var handler:Function;
			
			handler = function(event:Event):void
			{
				event.target.removeEventListener(Event.COMPLETE, handler);
				_classes = _classes.concat(classes);
			};
			
			dispatcher.addEventListener(Event.COMPLETE, handler, false, int.MAX_VALUE);
			
			return dispatcher;
		}
		
		// TODO: Refactor.
		// Actually can't use it until there is a way to enumerate proxy object silently.
		private function getMethodType(method:Function):Class
		{
			const obj:Object = getSavedThis(method);
			const methods:ClassType = ClassUtils.describeClassType(obj);
			const objName:String = methods.name;
			const methodNodes:XMLList = methods.descriptor.factory.method.(@declaredBy != objName); 
			const methodsLen:int = methodNodes.length();
			
			var methodNode:XML;
			
			for (var i:int = 0; i < methodsLen; i++) 
			{
				methodNode = methodNodes[i];
				
				if (obj[methodNode.@name] == method)
				{
					// TODO: Do we need any excluded types and exception preventing 
					// generalization of return type as it used for state handling?
					return ClassUtils.getClass(methodNode.@returnType.toString()) as Class;
				}
			}
			
			throw new Error("Can't resolve method return type.");
		}
		
		private function ensureInitialized():void
		{
			if (!_initialized)
				throw new Error("MediationManager should be initialized.");
		}
		
		public function get initialized():Boolean { return _initialized; }
		public function get mediationData():MediationData { return _mediationData; }
		public function get providerData():ProviderData { return _providerData; }
	}
}